package org.langcom.crypt;

import java.io.Serializable;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Arrays;

import javax.security.auth.x500.X500Principal;
import javax.security.auth.x500.X500PrivateCredential;

import org.apache.commons.lang.SerializationUtils;

/**
 * This utility class should be used for objects signing and packaging created
 * signature for further transportation. This way is better that
 * <code>java.security.SignedObject</code> provides in situations whether we
 * need to send a signature only but not an object itself with the signature.
 */
public final class DigitalSignature implements Serializable {

	static final long serialVersionUID = 27713196095203119L;

	private final byte[] signature;

	private final String algorithm;

	private final long timestamp;

	private final X500Principal issuer;

	/**
	 * Creates new signature object incapsulating signing process and storing
	 * the signature and used algorithm's name.
	 * 
	 * @param privateKey
	 *            the private key to sign object.
	 * @param object
	 *            the object to sign.
	 * @param engine
	 *            the signing engine.
	 * @throws SignatureException
	 *             may be thrown by the signing engine.
	 * @throws InvalidKeyException
	 *             may be thrown by the signing engine.
	 */
	public DigitalSignature(X500PrivateCredential credential, Serializable object, Signature engine) throws SignatureException, InvalidKeyException {
		this(credential, SerializationUtils.serialize(object), engine);
	}
	
	public DigitalSignature(X500PrivateCredential credential, byte[] toSign, Signature engine) throws SignatureException, InvalidKeyException {
		engine.initSign(credential.getPrivateKey());
		engine.update(toSign);
		signature = engine.sign();
		algorithm = engine.getAlgorithm();
		issuer = credential.getCertificate().getIssuerX500Principal();
		timestamp = System.currentTimeMillis();
	}

	public DigitalSignature(byte[] signature, String algorithm, long timestamp, X500Principal issuer) {
		this.signature = signature;
		this.algorithm = algorithm;
		this.timestamp = timestamp;
		this.issuer = issuer;
	}

	/**
	 * @return Returns the issuer.
	 */
	public X500Principal getIssuer() {
		return issuer;
	}

	/**
	 * Returns the signature created.
	 * 
	 * @return the signature.
	 */
	public byte[] getSignature() {
		return signature;
	}

	/**
	 * The signature's algorithm name.
	 * 
	 * @return the algorithm.
	 */
	public String getAlgorithm() {
		return algorithm;
	}

	/**
	 * Verifies the signature using the public key and the source object.
	 * 
	 * @param key
	 *            the public key to use.
	 * @param obj
	 *            the object to change signature of.
	 * @return Returns true only if exactly the source object was used while
	 *         signature creation using a private key corresponds to the public
	 *         key int their key pair. Otherwise returns false.
	 * @throws InvalidKeyException
	 *             may be thrown by signing engine.
	 * @throws NoSuchAlgorithmException
	 *             may be thrown by signing engine.
	 * @throws SignatureException
	 *             may be thrown by signing engine.
	 */
	public boolean verify(PublicKey key, Serializable obj) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
		return verify(key, SerializationUtils.serialize(obj));
	}

	/**
	 * @return Returns the timestamp.
	 */
	public long getTimestamp() {
		return timestamp;
	}

	/**
	 * @see java.lang.Object#equals(Object)
	 */
	public boolean equals(Object object) {
		if (!(object instanceof DigitalSignature)) {
			return false;
		}
		DigitalSignature ds = (DigitalSignature) object;
		return issuer.equals(ds.issuer)
			&& Arrays.equals(signature, ds.signature)
			&& (timestamp == ds.timestamp)
			&& algorithm.equals(ds.algorithm);
	}

	public int hashCode() {
		return (int) timestamp;
	}

	public boolean verify(PublicKey key, byte[] obj) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
		Signature engine = Signature.getInstance(algorithm);
		engine.initVerify(key);
		engine.update(obj);
		return engine.verify(signature);
	}
}